#!/usr/bin/perl

# == Slicer 4 Rotating Tilted Nozzle (RTN) 4-Axis Setup ==
#    written by Rene K. Mueller <spiritdude@gmail.com>
#
# Copyright: (see COPYRIGHT file)
# License: LGPLv3 (see LICENSE file)
#
# Description:
#   It provides simple conic slicer using ordinary slicer like Slic3r.
#
#   For technical details see at 
#        https://xyzdims.com/3d-printing/slicer4rtn
#     and
#        https://github.com/Spiritdude/Slicer4RTN
#
# History:
# 2021/06/02: 0.6.0: $efa multiplier adjusted, --rot-revolve=0 (new default, unlimited revolv), --rot-revolve=1 (single revolv) does performs smart rotate-around to immitate continenous rotation
# 2021/05/13: 0.5.2: --rotate, --scale and --translate added for pre-processing model
# 2021/05/05: 0.5.1: experimentally support .off, .obj, both more compact than .stl
# 2021/05/01: 0.5.0: fixing levelModel() affected CuraEngine/cura-slicer, version bump
# 2021/04/27: 0.4.8: support of start-gcode and end-gcode
# 2021/04/15: 0.4.7: cleaner X Y Z output
# 2021/03/23: 0.4.6: adding support for 'cura-slicer' (CuraEngine wrapper https://github.com/Spiritdude/Cura-CLI-Wrapper)
# 2021/03/22: 0.4.5: fixing config load for slic3r & prusa-slicer
# 2021/03/20: 0.4.2: CuraEngine 4.4.x & CuraEngineLegacy (15.10) experimental support added 
# 2021/03/16: 0.4.0: supporting ~/.config/slicer4rtn/slicer4rtn.ini with new defaults
# 2021/03/16: 0.3.4: rot_revolv=1 better $rot calculation (rot_revolv=0 untested)
# 2021/03/12: 0.3.1: changing rot-offset 0 => -90
# 2021/03/11: 0.3.0: version bump with various cleanups, more consistent settings, --inter-steps added
# 2021/03/11: 0.2.6: diverse new settings: rot_gcode, rot_offset, rot_revolv, tilt_gcode and layer-height which is computed for core-slicer
# 2021/03/10: 0.2.5: added main $efa multiplier depends on angle, not yet sure if it's correct
# 2021/03/08: 0.2.4: making arguments for core slicer easier, --slicer.<k>=<v> as well --<k>=<v> works (better for print3r integration)
# 2021/03/07: 0.2.3: simplifying code, extrusion interpolation still off (needs reworking)
# 2021/03/06: 0.2.0: changing extrusion calculation, better apprx., flow_rate => erate
# 2021/03/04: 0.1.1: properly re-offset G-code output from slic3r and prusa-slicer as it centers model, early support for prusa-slicer
# 2021/03/01: 0.1.0: new --slicer.key=val added, added --zoff=val to re-adjust zoffset in final G-code
# 2021/02/28: 0.0.5: added --axis=3, 4 or 5 to create for 3-, 4- or 5-axis G-code 3d-printer, added --angle=45 to change angle of cone
# 2021/02/26: 0.0.4: added --mode=outside or inside to reverse order for inside/outside-cone printing
# 2021/02/25: 0.0.3: moving stl2rtn and gcodertn into slicer4rtn
# 2021/02/24: 0.0.2: bug fixed, much better G-gcode output, viewable by Cura
# 2021/02/23: 0.0.1: first functional version, various bugs

use strict;
use Math::Trig;
use POSIX;
#use JSON;

my $APPNAME = 'Slicer4RTN';
my $VERSION = '0.6.0';

my($app) = ($0=~/\/([^\/]+)$/);

my $conf = {
   intern_format => 'stl',
   original_gcode => 1,
   slicer => 'slic3r',
   mode => 'outside',
   axis => 4,
   angle => 45,
   center => "0,0",
   bed_center => "100,100",
   layer_height => 0.2,
   zoff => 0,
   max_speed => 0,
   motion_minz => 0.2,
   erate => 1.0,
   efmax => 3,
   efmin => 0.01,
   inter_steps => 2,
   subdivide => 2,
   keep => 0,
   verbose => 0,
   recenter => 0,
   rot_gcode => 'A',
   rot_revolv => 0,
   rot_offset => -90,
   tilt_gcode => 'B',
};

my $confUnset = {
   output => 1,
   rot_fixed => 1,
   start_gcode => 1,
   end_gcode => 1, 
   rotate => 1,
   translate => 1,
   scale => 1
};

my %sf2l = ( 'v'=>'verbose', 'k'=>'keep' );
my(@slicer_args);

foreach my $p ("/usr/share/$app","$ENV{HOME}/.config/$app") {     # -- check system-wide and user settings
   open(my $fh,"<","$p/$app.ini");
   while(<$fh>) {
      chop;
      next if(/^\s*#/);
      my($k,$v);
      $k = $1, $v = $2, $k =~ s/-/_/g, $conf->{$k} = $v, next if(/^\s*([\w\-]+)\s*=\s*(\S.*)\s*$/);
      $k = $1, $k =~ s/-/_/g, $conf->{$k}++, next if(/^\s*([\w\-]+)\s*$/);
   }
   close $fh;
}

my @fs;
my @slicer_args_cli;
my @transform;

foreach(@ARGV) {                            # -- preprocess all command-line arguments
   my($s,$k,$v);

   if(/^--(slicer)\.([\w\-]+)=(.*)/) {      # -- slicer specific declared
      $s = $1, $k = $2, $v = $3;
      push(@slicer_args_cli,
         $conf->{slicer} eq 'mandoline' ? ("-S","$k=$v") : 
         $conf->{slicer} =~ /Cura/ ? ("-s","$k=$v") : "--$k=$v"
      );
      next;
   }
   if(/^--([\w\-\.]+)=(.*)/) {
      $k = $1, $v = $2;
      my $k_ = $k; $k_ =~ s/\-/_/g;
      if(!defined $conf->{$k_} && !defined $confUnset->{$k_}) {               # -- slicer args
         push(@slicer_args_cli,
            $conf->{slicer} eq 'mandoline' ? ("-S","$k=$v") : 
            $conf->{slicer} =~ /Cura/ ? ("-s","$k=$v") : "--$k=$v");
      } elsif($k_ =~ /^(rotate|translate|scale)$/) {
         push(@transform,"$k_=$v");
      } else {
         $conf->{$k_} = $v;                                                   # -- general settings
      }
      next;
   }
   $k = $1, $k =~ s/\-/_/g, $conf->{$k}++, next if(/^--([\w\-\.]+)/);         # -- general switches
   if(/^-(\w+)/) {                                                            # -- single character switch extended
      foreach(split(/|/,$1)) {
         $conf->{defined $sf2l{$_} ? $sf2l{$_} : $_}++;
      }
      next;
   }
   push(@fs,$_);                                                              # -- must be a file to process
}

if($conf->{version}) {
   print "$APPNAME $VERSION\n";
   exit 0;
}

my $lhc = $conf->{layer_height}/cos($conf->{angle}/180*pi());

# -- now we know slicer, add sane & important settings, and convert CLI opts into slicer args
if($conf->{slicer} =~ /(slic3r|prusa)/) {
   push(@slicer_args,'--gcode-comments','--skirts=0', ($conf->{slicer}=~/prusa/?'--center=':'--print-center=').("0,0"||$conf->{center}));
      #'--before-layer-gcode=;LAYER:[layer_num]');  # -- make it Cura compatible to catch layer change
   push(@slicer_args,'-s') if($conf->{slicer}=~/prusa/);    # -- slice G-code
   push(@slicer_args,'--layer-height='.$lhc);
   push(@slicer_args,'--start-gcode=;') if(defined $conf->{start_gcode});
   push(@slicer_args,'--end-gcode=;') if(defined $conf->{end_gcode});
   foreach my $s ($conf->{slicer}) {
      push(@slicer_args,"--load=/usr/share/$app/$s.ini") if(-e "/usr/share/$app/$s.ini");
      push(@slicer_args,"--load=$ENV{HOME}/.config/$app/$s.ini") if(-e "$ENV{HOME}/.config/$app/$s.ini");
   }

} elsif($conf->{slicer} =~ /mandoline$/) {
   push(@slicer_args,'-S','layer_height='.$lhc);
   push(@slicer_args,'--no-support','--no-raft','-n','-S','bed_center_x=0','-S','bed_center_y=0');
   push(@slicer_args,'--start-gcode=;') if(defined $conf->{start_gcode});
   push(@slicer_args,'--end-gcode=;') if(defined $conf->{end_gcode});
   foreach my $p ("/usr/share/$app","$ENV{HOME}/.config/$app") {
      my $c = parseConfig($p."/mandoline.ini");
      foreach my $k (keys %$c) {
         push(@slicer_args,'-S',"$k=$c->{$k}");
      }
   }
   
} elsif($conf->{slicer} =~ /CuraEngine$/) {
   push(@slicer_args,'-s','layer_height='.$lhc);
   push(@slicer_args,'-s','support_enable=false','-s','skirt_line_count=0','-s','brim_line_count=0','-s','support_brim_line_count=0');
   push(@slicer_args,'-s','machine_start_gcode=;') if(defined $conf->{start_gcode});
   push(@slicer_args,'-s','machine_end_gcode=;') if(defined $conf->{end_gcode});
   foreach my $p ("/usr/share/$app","$ENV{HOME}/.config/$app") {
      my $c = parseConfig($p."/CuraEngine.ini");
      foreach my $k (keys %$c) {
         push(@slicer_args,'-s',"$k=$c->{$k}");
      }
   }

} elsif($conf->{slicer} =~ /CuraEngineLegacy$/) {
   push(@slicer_args,'-s','autoCenter=0','-s','objectPosition.X=0','-s','objectPosition.Y=0','-s','layerThickness='.$lhc*1000,
      '-s','skirtLineCount=0','-s','downSkinCount=3','-s','upSkinCount=3','-s','filamentDiameter=1750');
   foreach my $p ("/usr/share/$app","$ENV{HOME}/.config/$app") {
      my $c = parseConfig($p."/CuraEngineLegacy.ini");
      foreach my $k (keys %$c) {
         push(@slicer_args,'-s',"$k=$c->{$k}");
      }
   }
} elsif($conf->{slicer} =~ /cura-slicer$/) {
   push(@slicer_args,"-vv") if($conf->{verbose});
   push(@slicer_args,'--layer_height='.$lhc);
   push(@slicer_args,'--machine_start_gcode=;') if(defined $conf->{start_gcode});
   push(@slicer_args,'--machine_end_gcode=;') if(defined $conf->{end_gcode});
   foreach my $p ("/usr/share/$app","$ENV{HOME}/.config/$app") {
      my $c = parseConfig($p."/cura-slicer.ini");
      foreach my $k (keys %$c) {
         push(@slicer_args,"--$k=$c->{$k}");
      }
   }
   push(@slicer_args,'--layer_height='.$lhc,'--support-enable=0','--brim-line-count=0','--skirt-line-count=0','--machine_center_is_zero=true');
}

push(@slicer_args,@slicer_args_cli);      # -- append CLI args as well (at least to override earlier settings from config files)

if($conf->{help} || @fs <= 0) {
   print "USAGE $APPNAME $VERSION: [<opts>] <file.stl> ...
   options:
      -v or --verbose      increase verbosity
      --version            display version and exit
      -k or --keep         keep all temporary files (temp.stl, temp.gcode)
      --rotate=<x,y,z>     rotate model
      --translate=<x,y,z>  translate model
      --scale=<s>          scale uniform
      --scale=<x,y,z>      scale individually
      --recenter           recenter model X- & Y-wise
      --subdivide=<n>      set midpoint subdivisions (default: $conf->{subdivide})
      --mode=<mode>        set cone mode, either 'outside' or 'inside' (default: '$conf->{mode}')
      --output=<fname>     override default naming convention file.stl -> file.gcode
      --axis=<axis>        set axis count of printer: 3, 4 or 5 (default: $conf->{axis})
      --angle=<angle>      set angle of cone (default: $conf->{angle})
      --center=<cx,cy>     set conic slicing center (default: $conf->{center})
      --bed-center=<cx,cy> set bed-enter, only affects output G-code (default: $conf->{bed_center})
      --layer-height=<z>   set conic layer height (default: $conf->{layer_height})
      --rot-gcode=<v>      set G-code symbol for rotation (default: $conf->{rot_gcode})
      --rot-revolv=<mode>  set rotation revolution, 0 = unlimited, 1 = once (default: $conf->{rot_revolv})
      --rot-offset=<a>     set rotation offset (default: $conf->{rot_offset})
      --rot-fixed=<angle>  set fixed rotation angle, usable if --axis=3 but 4-axis or 5-axis printer is target
      --tilt-gcode=<v>     set G-code symbol for tilt for 5-axis operation (default: $conf->{tilt_gcode})
      --zoff=<v>           set z-offset, will be added to G1 ... Z<v>
      --erate=<f>          set extrusion rate (multiplier, default: $conf->{erate})
      --efmin=<v>          set extrusion factor minimum, (default: $conf->{efmin})
      --efmax=<v>          set extrusion factor maximum, (default: $conf->{efmax})
      --inter-steps=<n>    set interpolation steps per mm (default: $conf->{inter_steps})
      --motion-minz=<v>    set minimum Z level for motion (without extrusion) (default: $conf->{motion_minz})
      --max-speed=<s>      set maximum speed (default: $conf->{max_speed})
      --slicer=<slicer>    set core slicer slic3r, prusa-slicer, CuraEngine{Legacy}, cura-slicer, mandoline (default: '$conf->{slicer}')
      --start-gcode=...    set start gcode (disables core slicer's start-gcode)
      --end-gcode=...      set end gcode (disables core slicer's end-gcode)
      --slicer.<k>=<v>     add additional slicer arguments, e.g. --slicer.infill-density=0
      --<k>=<v>            all other arguments not for slicer4rtn will be passed to the core slicer ($conf->{slicer})
      
   examples:
      $app sphere.stl
      $app overhang.stl --output=sample.gcode
      $app overhang.stl --axis=5 --output=sample.gcode
      $app overhang.stl --axis=3 --output=sample-belt-printer.gcode --fill-density=5
      $app model-6.stl --angle=25 --subdivide=5

";
   exit 0;
}

my($cx,$cy) = split(/,/,$conf->{center}) if($conf->{center});

print "== $APPNAME $VERSION == https://github.com/Spiritdude/$APPNAME\n";

$| = 1;     # -- non-buffer stdout

foreach my $fn (@fs) {
   my $t = time();
   my @rm;
   
   unless(-e $fn) {
      print STDERR "$app ERROR: file not accessible '$fn'\n";
      exit -1;
   }
   print "processing '$fn':\n";
   print "   1/5 read model\n";
   my $m = readModel($fn);

   measureModel($m);
   recenterModel($m) if($conf->{recenter});

   foreach my $t (@transform) {
      my($type,$v) = ($t=~/(\w+)=(.*)/);
      print "      transform: $type $v\n";
      if($type eq 'rotate') {
         $m = rotateModel($m,[split(/,/,$v)]);
      } elsif($type eq 'translate') {
         $m = translateModel($m,[split(/,/,$v)]);
      } elsif($type eq 'scale') {
         $m = scaleModel($m,[split(/,/,$v)]);
      }
   }
   measureModel($m) if(@transform>0);
   
   # print to_json($m,{pretty=>1,canonical=>1});
   
   for(my $n=1; $n<=$conf->{subdivide}; $n++) {
      print "      $n/$conf->{subdivide} subdivide ";
      $m = subdivideModel($m);
   }
   measureModel($m) if($conf->{subdivide}>0);
   
   print "   2/5 map vertices\n";
   mapVertices($m);
   
   measureModel($m), levelModel($m); # if($conf->{slicer}=~/Cura/);
   
   my($tmp_gcode,$tmp_model) = ("./tmp-$$.gcode","./tmp-$$.$conf->{intern_format}");
   print "   3/5 write temporary model\n";

   $tmp_model =~ s/\.\w+$/.stl/ if($conf->{slicer} =~ /(cura|mandoline)/i);      # -- cura & mandoline only manage STL

   writeModel($tmp_model,$m);
   push(@rm,$tmp_model);

   print "   4/5 slice ($conf->{slicer}) model\n";
   if(fork()==0) {
      my(@a) = ($conf->{slicer});
      push(@a,$conf->{slicer} =~ /CuraEngine$/ ? 
         ("slice","-j","/usr/share/$app/fdmprinter.def.json","-s","machine_center_is_zero=true","-s","extruder_nr=0",@slicer_args,"-o",$tmp_gcode,"-l",$tmp_model) :
         (@slicer_args,"-o",$tmp_gcode,$tmp_model) );
      print "$app INF: @a\n" if($conf->{verbose});
      # -- is important, as CuraEngine is chatty and closing only STDERR will pollute the resulting G-code with error messages(!!)
      close STDERR, close STDOUT if($conf->{slicer} =~ /CuraEngine$/ && $conf->{verbose}==0);      
      exec(@a);
      exit 0;
   }
   wait;
   if(!-e "$tmp_gcode") {
      print "$app ERROR: slicer did not generate any gcode, abort.\n";
      print "$app HINT: execute with -v to see the actual problem\n" unless($conf->{verbose});
      print "$app HINT: increase --subdivide=.. more and try again, or use --slicer=slic3r instead\n" if($conf->{slicer}=~/prusa/);
      unlink @rm unless($conf->{keep});
      exit -1;
   }
   push(@rm,$tmp_gcode);
   my $fo = $fn;

   $fo =~ s/\.(stl|amf|obj|off|3mf)$/.gcode/i;
   $fo .= ".gcode" unless($fo=~/\.gcode$/i);
   
   $fo = $conf->{output} if($conf->{output});
   print "   5/5 remap gcode to '$fo'";
   print "\n" if($conf->{verbose});

   my $ln = mapGcode($tmp_gcode,$fo,$m);
   print " ($ln lines)\n";
   
   if($conf->{keep}) {
      print "$app: $tmp_model and $tmp_gcode kept\n";
   } else {
      unlink @rm;
   }
   $t = time()-$t;
   print sprintf("== took %d sec%s total, done.\n",$t,$t>1?"s":"");
}

sub conicSpaceMapping {
   my($cx,$cy,$x,$y,$z,$dir) = @_;
   my $dx = $x-$cx; 
   my $dy = $y-$cy;
   my $d = sqrt($dx*$dx + $dy*$dy);
   my $rot = atan2($dy,$dx)/pi()*180;     # -180 .. 180

   if($conf->{axis}==3) {
      #$d = -$y, $rot = 90;
      $rot = $conf->{rot_fixed} + $conf->{rot_offset};
      my $a = $rot/180*pi();
      $d = cos($a)*$x + sin($a)*$y;
   }
   $d *= tan($conf->{angle}/180*pi());
   
   $rot = $rot + 180 if($conf->{mode}eq'inside');
   
   $rot = sprintf("%.3f",$rot);
   
   return ($x,$y,$dir eq "direct" ? $z-$d : $z+$d,$rot);
}

sub measureModel {
   my($m) = @_;
   my(@min,@max);
   @min = (1e38,1e38,1e38);
   @max = (-1e38,-1e38,-1e38);
   foreach my $p (@{$m->{vertices}}) {
      for(my $i=0; $i<3; $i++) {
         $min[$i] = $p->{c}->[$i] if($min[$i]>$p->{c}->[$i]);
         $max[$i] = $p->{c}->[$i] if($max[$i]<$p->{c}->[$i]);
      }
   }
   $m->{size} = [ $max[0]-$min[0], $max[1]-$min[1], $max[2]-$min[2] ];
   $m->{min} = \@min;
   $m->{max} = \@max;
   #print to_json([$m->{size},$m->{min},$m->{max}]);
}

sub recenterModel {
   my($m) = @_;
   foreach my $v (@{$m->{vertices}}) {
      $v->{c}->[0] -= ($m->{max}->[0]+$m->{min}->[0])/2;
      $v->{c}->[1] -= ($m->{max}->[1]+$m->{min}->[1])/2;
      #print "OUT: $v->{c}->[0],$v->{c}->[1],$v->{c}->[2]\n";
   }
   measureModel($m);
}

sub levelModel {
   my($m) = @_;
   foreach my $v (@{$m->{vertices}}) {
      $v->{c}->[2] -= $m->{min}->[2];
   }
   measureModel($m);
}

sub mapVertices {
   my($m) = @_;
   foreach my $v (@{$m->{vertices}}) {
      #print "IN: $v->{c}->[0],$v->{c}->[1],$v->{c}->[2]\n";
      my($x1,$y1,$z1) = conicSpaceMapping($cx,$cy,$v->{c}->[0],$v->{c}->[1],$v->{c}->[2],$conf->{mode}eq'inside'?'direct':'inverse');
      $v->{c}->[0] = $x1;
      $v->{c}->[1] = $y1;
      $v->{c}->[2] = $z1;
      #print "OUT: $v->{c}->[0],$v->{c}->[1],$v->{c}->[2]\n";
   }
}

sub mapGcode {
   my($fn,$fo,$m) = @_;
   my $z = 0;
   my($lx,$ly,$lz,$le) = (0,0,0,0);

   my $mo = $conf->{mode}eq'inside'?'inverse':'direct';
 
   my $xtr = $conf->{axis}==5 || defined $conf->{rot_fixed} ? $conf->{tilt_gcode}.$conf->{angle}." " : "";
 
   my($xoff,$yoff) = split(/,/,$conf->{bed_center});
   my $zoff = 0;    
   
   my($lxc,$lyc,$lzc) = (0,0,0,0);    # -- last conic coordinates
   my $eabs = 0;        # -- this is the conic E
   my $ef = 1;          # -- this is the current E factor (changes)
   my $rabs = 0;        # -- rotation angle absolute (for rot_revolv=0 unlimited revolutions)

   my $tot_ln = 0;
   
   # == Slic3r & PrusaSlicer ==
   # Original STL: is not centered, but with defined conic center
   # G-code output: it centers x/y-wise
   #   hence, we need to change xoff,yoff to recenter according original STL
   #
   #           STL                  G-code
   #     XXXXXXXXXXXXXXXXX    XXXXXXXXXXXXXXXXX
   #     XXX                  XXX     |
   #      |                   a|--b---|---c----
   #      0                           0
   #
   #     resolve for b:  b = tot - a - c
   #                   off = tot - min - tot/2
   
   my($ixoff,$iyoff) = (0,0);
   if($conf->{slicer}=~/(slic3r|prusa)/) {
      $ixoff = $m->{size}[0] - abs($m->{min}[0]) - $m->{size}[0] / 2;
      $iyoff = $m->{size}[1] - abs($m->{min}[1]) - $m->{size}[1] / 2;
   }
   #print "==> $ixoff, $iyoff\n";
   #print to_json([$m->{size},$m->{min},$m->{max}],{canonical=>1,pretty=>1});
   
   if(1) {     # -- we determine the Z minimum of to be expected final G-code output
      open(my $fh,"<",$fn) || die "$app: cannot read '$fn'\n";
      my($c);
      my($zmin,$zmax) = (1e38,-1e38);
      while(<$fh>) {
         $tot_ln++;
         next if(/^\s*;/);
         my $l = $_;
         if($l =~ /^G[01] /) {
            my $s;
            while($l =~ s/ ([XYZ])([\-\d\.]+)//) {
               $c->{$1} = $2;
               $s++;
            }
            if($s && $l =~ / E([\d\.]+)/) {     # -- only consider actual positive extrusion coordinates
               if(defined $c->{X} && defined $c->{Y} && defined $c->{Z}) {
                  my($x1,$y1,$z1) = conicSpaceMapping($cx,$cy,$c->{X}+$ixoff,$c->{Y}+$iyoff,$c->{Z},$mo);
                  if($zmin>$z1) {
                     $zmin = $z1;
                     print "$app: zmin calc\n  $l  $c->{X},$c->{Y},$c->{Z} -> $x1,$y1,$z1\n" if($conf->{verbose}>1);
                  }
                  if($zmax<$z1) {
                     $zmax = $z1;
                     print "$app: zmax calc\n  $l  $c->{X},$c->{Y},$c->{Z} -> $x1,$y1,$z1\n" if($conf->{verbose}>1);
                  }
               }
            }
         }
      }
      print "$app INF: zmin = $zmin\n" if($conf->{verbose});
      print "$app INF: zmax = $zmax\n" if($conf->{verbose});
      $zoff = -$zmin;        # -- that will become the negative z-offset => move transformed piece back to z=0
      close $fh;
   }

   open(my $fh,"<",$fn) || die "$app: ERROR: cannot read '$fn'\n";
   open(my $fho,">",$fo) || die "$app: ERROR: cannot write '$fo'\n";
   print $fho "; == $APPNAME $VERSION == https://github.com/Spiritdude/Slicer4RTN\n; Date: ".scalar localtime()."\n; Settings:\n";
   foreach my $k (sort keys %$conf) {
      print $fho ";   $k = $conf->{$k}\n";
   }
   print $fho "; Slicer-specific settings: ".join(' ',map{ "'$_'" } @slicer_args)."\n";
   print $fho ";\n";

   if(defined $conf->{start_gcode}) {
      print $fho "; slicer4rtn start-gcode:\n";
      my $g = "$conf->{start_gcode}\n";
      $g =~ s/\\n/\n/g;
      print $fho "$g; /slicer4rtn start-gcode\n";
   }

   my $ln = 0;       # -- line number of Gcode
   my $lyn = 0;      # -- layer number (0..n)
   my $zcmax = 0;

   my $efa = cos($conf->{angle}/180*pi());         # -- this is the main extrusion correction ratio (multiplier)

   my $first_move_done = 0;
   my $lrot;

   while(<$fh>) {
      chop;
      # -- IMPORTANT:
      #    $x,$y,$z,$e are original G-code values (not conic space transformed)
      #    $lx,$ly,$lz,$le are the "last" values, also in original G-code values (not conic space transformed)
      #        do not alter them, read-only
      #    $x0,$y0,$z0 or $x1,$y1,$z1 are temporary variables 
      #    $lxc,$lyc,$lzc are the last conic coords
      #
      #    *** DO NOT MIX THOSE, otherwise you create a mess (literally on your printbed) ***
      
      print $fho "; $_\n" if(/^G[01] / && $conf->{original_gcode});     # -- maintain original unlatered G-code (for now)

      if(/^G[01] (.*)X([\-\d\.]+) Y([\-\d\.]+)(.*)$/) {      # -- moving X/Y in the vertical layer
         my($p,$x,$y,$r) = ($1,$2,$3,$4);

         if(1 && / Z([\-\d\.]+)/) {       # -- slic3r doesn't use X, Y and Z, only G1 Z.. for layer change, but other slicers might like Cura or Simplify3D
            $z = $1;
            $p =~ s/ Z[\-\d\.]+//;
            $r =~ s/ Z[\-\d\.]+//;
         }
         if(1 && / F([\-\d\.]+)/ && $1 > $conf->{max_speed} && $conf->{max_speed} && / E[\-\d\.]+/) {    
            $p =~ s/F[\-\d\.]+/F$conf->{max_speed}/;
            $r =~ s/F[\-\d\.]+/F$conf->{max_speed}/;
         }
         
         my($d) = sqrt(($x-$lx)*($x-$lx)+($y-$ly)*($y-$ly)+($z-$lz)*($z-$lz));

         if(/ E([\-\d\.]+)/ ) {         # -- extruding
            my $e = $1;
            my $steps = int($d*$conf->{inter_steps}+1);

            $steps = 2 if($steps<2);
            $steps = 2 if($d<2);

            $r =~ s/ E[\-\d\.]+//;  # -- remove E.. from $r(est), we add it again with new value
            $p =~ s/ E[\-\d\.]+//;  # -- remove E.. from $p(re), we add it again with new value
            
            for(my $n=1; $n<=$steps; $n++) {       # -- single G1 extrusion segment will be sub-segmented in $steps
               my $f = $n/$steps;      # -- fader 1.0/steps .. 1.0

               my($x0,$y0,$z0,$e0) = ((1-$f)*$lx + $f*$x, (1-$f)*$ly + $f*$y, (1-$f)*$lz + $f*$z, (1-$f)*$le + $f*$e);

               $x0 = sprintf("%.5f",$x0);
               $y0 = sprintf("%.5f",$y0);
               $z0 = sprintf("%.5f",$z0);
               $e0 = sprintf("%.5f",$e0);

               $x0 += $ixoff; $y0 += $iyoff;    # -- realign G-code coords before remapping again

               my($x1,$y1,$z1,$rot) = conicSpaceMapping($cx,$cy,$x0,$y0,$z0,$mo);

               $rabs -= fmod($rabs,360)-180-$rot;
               $rot = $rabs + $conf->{rot_offset} if($conf->{rot_revolv}==0);
               #$rot = fmod($rot+360+$conf->{rot_offset},360)-180 if($conf->{rot_revolv}==1);
               if($conf->{rot_revolv}==1) {
                  $rot = fmod($rot+360+$conf->{rot_offset},360)-180;
                  if(!defined $conf->{rot_fixed} && defined $lrot && abs($lrot-$rot)>180 && sqrt($x1*$x1+$y1*$y1) > 1) {
                     my $g = rotate_around($lrot,$rot,sqrt($x1*$x1+$y1*$y1),{xoff=>$xoff,yoff=>$yoff});
                     print $fho join('',@$g);
                  }
               }
               $rot = $conf->{rot_fixed} if(defined $conf->{rot_fixed});
               
               $x1 += $xoff;
               $y1 += $yoff;
               $z1 += $zoff;
               $z1 += $conf->{zoff};

               $x1 = sprintf("%.5f",$x1);
               $y1 = sprintf("%.5f",$y1);
               $z1 = sprintf("%.5f",$z1);
               $rot = sprintf("%.3f",$rot);
               $lrot = $rot;

               if(1) {
                  # -- I'm aware $do does not need to be calculated here (can move outside of for() but I keep it here until interpolation calculation is finalized)
                  my $do = sqrt(($lx-$x)*($lx-$x)+($ly-$y)*($ly-$y)+($lz-$z)*($lz-$z)) / $steps;      # -- distance original / step
                  my $dc = sqrt(($lxc-$x1)*($lxc-$x1)+($lyc-$y1)*($lyc-$y1)+($lzc-$z1)*($lzc-$z1));   # -- distance conic delta / step

                  # -- we interpolate extrusion * conic-travel-distance / original-travel-distance; $eabs = absolute extrusion
                  $ef = $dc / (abs($do)>0.0001 ? $do : 1);
                  $ef = $conf->{efmax} if($ef>$conf->{efmax});
                  $ef = $conf->{efmin} if($ef<$conf->{efmin});

                  my $ed = ($e - $le) / $steps * $ef * $efa * $conf->{erate}; $eabs += $ed;

                  print $fho "; do=$do, dc=$dc, ef=$ef ed=$ed\n" if($conf->{original_gcode} && $conf->{verbose});
                  $eabs = sprintf("%.5f",$eabs);

                  print $fho "G1 ${p}X$x1 Y$y1 Z$z1 E$eabs $conf->{rot_gcode}$rot $xtr$r; extrusion ".sprintf("%.2f",$f)." ($n of $steps)\n";

                  $lxc = $x1; $lyc = $y1; $lzc = $z1;

                  $zcmax = $z1 if($zcmax < $z1);      # -- track highest point of actual print

               } else {
                  $e0 *= $conf->{erate};
                  print $fho "G1 X$x1 Y$y1 Z$z1 E$e0 A$rot $xtr$r; extrusion $f ($n of $steps)\n";
               }
               $p =~ s/F[\-\d\.]+//;      # -- for next segments do not need feed rate (speed) anymore
               $r =~ s/F[\-\d\.]+//;      # -- for next segments do not need feed rate (speed) anymore
            }

            $lx = $x;
            $ly = $y;
            $lz = $z;
            $le = $e;

         } else {     # -- motion without extrusion (important to cone map as well, otherwise nozzle crashed into existing prints)
            my $steps = $first_move_done ? 3 : 1;        # -- first move perform motion direct, otherwise conic mapped
            my $e = 0;
            print $fho "; from $lz to $z (zoff=$zoff,conf.zoff=$conf->{zoff})\n" if($conf->{verbose});
            
            for(my $n=1; $n<=$steps; $n++) {
               my $f = $n/$steps;
               my($x0,$y0,$z0,$e0) = ((1-$f)*$lx + $f*$x, (1-$f)*$ly + $f*$y, (1-$f)*$lz + $f*$z, (1-$f)*$le + $f*$e);
               
               $x0 += $ixoff; $y0 += $iyoff;
               
               my($x1,$y1,$z1,$rot) = conicSpaceMapping($cx,$cy,$x0,$y0,$z0,$mo);
               
               $rabs -= fmod($rabs,360)-180-$rot;
               $rot = $rabs+$conf->{rot_offset} if($conf->{rot_revolv}==0);
               if($conf->{rot_revolv}==1) {
                  $rot = fmod($rot+360+$conf->{rot_offset},360)-180;
                  if(!defined $conf->{rot_fixed} && defined $lrot && abs($lrot-$rot)>180 && sqrt($x1*$x1+$y1*$y1) > 1) {
                     my $g = rotate_around($lrot,$rot,sqrt($x1*$x1+$y1*$y1),{xoff=>$xoff,yoff=>$yoff});
                     print $fho join('',@$g);
                  }
               }
               $rot = $conf->{rot_fixed} if(defined $conf->{rot_fixed});
               
               $x1 += $xoff;
               $y1 += $yoff;
               $z1 += $zoff;
               $z1 += $conf->{zoff};
               
               $x1 = sprintf("%.5f",$x1);
               $y1 = sprintf("%.5f",$y1);
               $z1 = sprintf("%.5f",$z1);
               $z1 = $conf->{motion_minz} if($z1 < $conf->{motion_minz});
   
               $rot = sprintf("%.3f",$rot);
               $lrot = $rot;

               print $fho "G1 ${p}X$x1 Y$y1 Z$z1 $conf->{rot_gcode}$rot $xtr$r; motion ".sprintf("%.2f",$f)." ($n of $steps)\n";

               $lxc = $x1; $lyc = $y1; $lzc = $z1;
               $p =~ s/F[\-\d\.]+//;            # -- for next segments do not need feed rate (speed) anymore
               $r =~ s/F[\-\d\.]+//;            # -- for next segments do not need feed rate (speed) anymore
            }
            $first_move_done++ unless($first_move_done);
               
            $lx = $x;
            $ly = $y;
            $lz = $z;
         }
      } elsif(/^G1 / && / Z([\-\d\.]+)(.*)/) {        # -- just layer change (without X/Y being mentioned): update z only
         $z = $1;
         $lz = $z;
         $lyn++;
         $rabs = fmod($rabs,360);                     
         print $fho "M117 Layer #$lyn\n" if($conf->{display_layer_number});
         print $fho ";LAYER:$lyn\n";                  # -- make it look like Cura G-code (for print3r and other backends)
         
      } elsif(/^G1 / && /(.+)E([\-\d\.]+)(.*)/) {     # -- (un)retract absolute (without X/Y being mentioned)
         my($p,$r) = ($1,$3);
         my $e = $2;
         my $e0 = $e*$conf->{erate};               
         my $ed = ($e - $le) * 1; $eabs += $ed;       # -- Note: $ed should be * $efa but we omit it as this is only happening when retract/unretract (slic3r)
         #print $fho "${p}E$e0$r; direct.c\n";
         print $fho "${p}E$eabs$r; direct.c ($ed)\n";
         $le = $e;

      } elsif(/^G92 / && /E([\d\.]*)/) {              # -- reset extruding count
         print $fho "$_\n";
         $le = $1;
         $eabs = $le;

      } elsif(/^G28 / && $ln > 100) {                 # -- hackish attempt to find end of print before homing
         $zcmax += 1;
         # -- end of print MAY NOT at the highest point, and G28 X might crash into the existing piece
         #    therefore we move nozzle up Z of maximum of print plus 1mm and that is a safe place now
         print $fho "G1 Z$zcmax ; lift nozzle to max of Z of actual print\n"; 
         print $fho "$_\n";
         
      } else {                                         # -- all the rest of G-code pass on unaltered
         print $fho "$_\n";
      }
      $ln++;
   }      
   
   if(defined $conf->{end_gcode}) {
      print $fho "; slicer4rtn end-gcode:\n";
      my $g = "$conf->{end_gcode}\n";
      $g =~ s/\\n/\n/g;
      print $fho "$g; /slicer4rtn end-gcode\n";
   }

   close $fh;
   close $fo;

   return $tot_ln;
}

sub rotate_around {
   my($lrot,$rot,$d,$opts) = @_;
   
   my $n = int(abs($lrot-$rot)*$d/36);        # 360deg * 10mm = 3600 / 36 = 100 segments
   $n = 2 if($n<2);
   $n = 100 if($n>100);
   
   my @g;
   
   for(my $i=1; $i<$n; $i++) {
      my $f = $i/$n;
      my $a = $lrot*(1-$f) + $rot*$f;                          # -- interpolate linearly
      my $ar = ($a + $conf->{rot_offset}) / 180 * pi();        # -- calculate angle in math realm
      my $d2 = $d + 1;

      my($x,$y) = (cos($ar)*$d2, sin($ar)*$d2);                # -- recalculate x,y with an additional 1mm to rotate around

      $x += $opts->{xoff};
      $y += $opts->{yoff};

      $x = sprintf("%.5f",$x);
      $y = sprintf("%.5f",$y);
      $a = sprintf("%.3f",$a);

      push(@g,"G1 X$x Y$y $conf->{rot_gcode}$a ; rotate around ($i of $n steps, $lrot => $rot)\n");
   }
   return \@g;
}

sub subdivideModel {
   my($m) = @_;
   my $mn = { volumes => [ { triangles => [ ] } ] , vertices => [ ] };

   my $vn = 0;
   foreach my $v (@{$m->{vertices}}) {       # -- copy vertices
      push(@{$mn->{vertices}},$v);
      $vn++;
   }
   foreach my $f (@{$m->{volumes}->[0]->{triangles}}) {
      my $pn0 = $f->{v}->[0];
      my $pn1 = $f->{v}->[1];
      my $pn2 = $f->{v}->[2];
      my($p0,$p1,$p2) = ($m->{vertices}->[$pn0]->{c},$m->{vertices}->[$pn1]->{c},$m->{vertices}->[$pn2]->{c});

      my $m0 = midpoint($p0,$p1);
      my $m1 = midpoint($p1,$p2);
      my $m2 = midpoint($p2,$p0);

      push(@{$mn->{vertices}},{c=>$m0}); # 0
      push(@{$mn->{vertices}},{c=>$m1}); # 1
      push(@{$mn->{vertices}},{c=>$m2}); # 2
      
      #           p0
      #          / \
      #         / a \
      #     2/m2.....m0/0
      #       /.     .\
      #      /  . d .  \
      #     / c  . . b  \
      #    p2----m1-----p1
      #          /1
      
      push(@{$mn->{volumes}->[0]->{triangles}},{ v => [$pn0, $vn+0, $vn+2] });  # a
      push(@{$mn->{volumes}->[0]->{triangles}},{ v => [$vn+0, $pn1, $vn+1] });  # b
      push(@{$mn->{volumes}->[0]->{triangles}},{ v => [$vn+2, $vn+1, $pn2] });  # c
      push(@{$mn->{volumes}->[0]->{triangles}},{ v => [$vn+0, $vn+1, $vn+2] }); # d

      $vn += 3;
   }
   print "($vn vertices)\n";
   return $mn;
}

sub midpoint {
   my($p0,$p1) = @_;
   my($pm);
   for(my $i=0; $i<3; $i++) {
      $pm->[$i] = ($p0->[$i] + $p1->[$i]) / 2;
   }
   return $pm;
}

sub modelTransform {
   my($m,$f) = @_;
   my $n = @{$m->{vertices}};
   for(my $i=0; $i<$n; $i++) {
      my(@pn) = &$f(@{$m->{vertices}->[$i]->{c}});
      $m->{vertices}->[$i]->{c} = \@pn;
   }
   return $m;
}

sub scaleModel {
   my($m,$t) = @_;
   $t = [$t->[0],$t->[0],$t->[0]] if(@$t < 3);
   $m = modelTransform($m,sub { 
      my(@p) = @_;
      return($p[0]*$t->[0],$p[1]*$t->[1],$p[2]*$t->[2]);
   });
   return $m;
}

sub translateModel {
   my($m,$t) = @_;
   $m = modelTransform($m,sub { 
      my(@p) = @_;
      return($p[0]+$t->[0],$p[1]+$t->[1],$p[2]+$t->[2]);
   });
   return $m;
}

sub rotateModel {
   my($m,$r) = @_;
   my(@rs) = ($r->[0] * pi/180,$r->[1] * pi/180,$r->[2] * pi/180);

   $m = modelTransform($m,sub { 
      my(@p) = @_;
      my(@pn);
      # -- x
      $pn[0] = $p[0];  
      $pn[1] = $p[1]*cos($rs[0]) - $p[2]*sin($rs[0]);
      $pn[2] = $p[1]*sin($rs[0]) + $p[2]*cos($rs[0]);
      @p = @pn;
      # -- y
      $pn[0] = $p[0]*cos($rs[1]) + $p[2]*sin($rs[1]);
      $pn[1] = $p[1];
      $pn[2] = $p[2]*cos($rs[1]) - $p[0]*sin($rs[1]);
      @p = @pn;
      # -- z
      $pn[0] = $p[0]*cos($rs[2]) - $p[1]*sin($rs[2]);
      $pn[1] = $p[0]*sin($rs[2]) + $p[1]*cos($rs[2]);
      $pn[2] = $p[2];
      return (@pn);
   });
   return $m;
}   

sub parseConfig {
   my($fn) = @_;
   my $c = {};

   return unless(-e $fn);

   print "$app: reading $fn\n" if($conf->{verbose});

   open(my $fh,"<",$fn);
   my($k,$v);
   while(<$fh>) {
      chop;
      next if(/^\s*#/);
      $k = $1, $v = $2, $c->{$k} = $v if(/(\w+)\s*=\s*(.*)/);
   }
   close $fh;
   return $c;
}

sub readModel {
   my($fn) = @_;
   if($fn =~ /\.off$/i) {
      return readOFF($fn);
   } elsif($fn =~ /\.obj$/i) {
      return readOBJ($fn);
   } elsif($fn =~ /\.stl$/i || $fn =~ /\.stl[ab]$/i) {
      return readSTL($fn);
   } else {
      print "ERROR: file-format \"$fn\" not supported (only .stl, .off or .obj)\n";
      exit -1;
   }
}
   
sub writeModel {
   my($fn,$m) = @_;
   if($fn =~ /\.off$/i) {
      writeOFF($fn,$m);
   } elsif($fn =~ /\.obj$/i) {
      writeOBJ($fn,$m);
   } elsif($fn =~ /\.stla$/i) {
      writeSTLA($fn,$m);
   } elsif($fn =~ /\.stl$/i) {
      writeSTLB($fn,$m);
   } else {
      print "ERROR: file-format \"$fn\" not supported (only .stl, .off or .obj)\n";
      exit -1;
   }
}

sub writeSTLA {
   my($fn,$m) = @_;
   open(my $fh,">",$fn);
   print $fh "solid model\n";
   foreach my $v (@{$m->{volumes}}) {
      foreach my $f (@{$v->{triangles}}) {
         print $fh " facet normal 0 0 1\n";
         print $fh " outer loop\n";
         foreach my $v (@{$f->{v}}) {
            print $fh "  vertex ".join(" ",@{$m->{vertices}->[$v]->{c}})."\n";
         }
         print $fh " endloop\n";
         print $fh " endfacet\n";
      }
   }
   print $fh "endsolid model\n";
}

sub writeSTLB {
   my($fn,$m) = @_;
   # UINT8[80]  Header
   # UINT32  Number of triangles
   # 
   # foreach triangle
   # REAL32[3]  Normal vector
   # REAL32[3]  Vertex 1
   # REAL32[3]  Vertex 2
   # REAL32[3]  Vertex 3
   # UINT16  Attribute byte count
   # end
   open(my $fh,">",$fn);
   print $fh " "x80;
   my $ft = 0;
   foreach my $v (@{$m->{volumes}}) {
      $ft += scalar @{$v->{triangles}};
   }
   print $fh pack("L",$ft);
   foreach my $v (@{$m->{volumes}}) {
      foreach my $f (@{$v->{triangles}}) {
         print $fh pack("f3",0,0,1);
         print $fh pack("f3",@{$m->{vertices}->[$f->{v}->[0]]->{c}});
         print $fh pack("f3",@{$m->{vertices}->[$f->{v}->[1]]->{c}});
         print $fh pack("f3",@{$m->{vertices}->[$f->{v}->[2]]->{c}});
         print $fh pack("S",0);
      }
   }
   close $fh;
}

sub readSTL {
   my($fn) = @_;
   my $buff;
   my $m;
   my %c;                           # -- coordinate cache
   
   open(my $fh,"<",$fn);
   read($fh,$buff,256);
   seek($fh,0,0);    # -- rewind

   if($buff=~/facet/) {
      <$fh>;
      my $f;
      my @ci;
      my $cn = 0;
      while(<$fh>) {
         $f = 1, next if(/\s*outer loop/);
         if($f && /\s*endloop/) {
            push(@{$m->{volumes}->[0]->{triangles}},{v=>[@ci]});
            @ci = ();
            $f = 0, next;
         }
         if($f && /\s*vertex\s+(\S.*)/) {
            my(@v) = split(/\s+/,$1);
            my $fp = join(" ",@v);
            if(!defined $c{$fp}) {
               push(@{$m->{vertices}},{c=>\@v});
               $c{$fp} = $cn++;
            }
            push(@ci,$c{$fp});
         }
      }
      #$info{file_type} = "ascii";
   } else {
      # -- binary stl
      # UINT8[80]  Header
      # UINT32  Number of triangles
      # 
      # foreach triangle
      # REAL32[3]  Normal vector
      # REAL32[3]  Vertex 1
      # REAL32[3]  Vertex 2
      # REAL32[3]  Vertex 3
      # UINT16  Attribute byte count
      # end
      my $buff;

      read($fh,$buff,80);
      read($fh,$buff,4);
      my $n = unpack("L",$buff);
      my $cn = 0;
      
      for(my $i=0; $i<$n; $i++) { 
         my(@ci);
         my $fp;
         
         # -- normals
         read($fh,$buff,3*4); my(@v) = unpack("f3",$buff);  

         # -- v1
         read($fh,$buff,3*4); my(@v) = unpack("f3",$buff);  
         $fp = join(" ",@v);
         if(!defined $c{$fp}) {
            push(@{$m->{vertices}},{c=>\@v});
            $c{$fp} = $cn++;
         }
         push(@ci,$c{$fp});

         # -- v2
         read($fh,$buff,3*4); my(@v) = unpack("f3",$buff);  
         $fp = join(" ",@v);
         if(!defined $c{$fp}) {
            push(@{$m->{vertices}},{c=>\@v});
            $c{$fp} = $cn++;
         }
         push(@ci,$c{$fp});

         # -- v3
         read($fh,$buff,3*4); my(@v) = unpack("f3",$buff);  
         $fp = join(" ",@v);
         if(!defined $c{$fp}) {
            push(@{$m->{vertices}},{c=>\@v});
            $c{$fp} = $cn++;
         }
         push(@ci,$c{$fp});
         
         push(@{$m->{volumes}->[0]->{triangles}},{v=>\@ci});
         
         read($fh,$buff,2);
         my $an = unpack("S",$buff);
      }
      #$info{file_type} = "binary";
   }
   close $fh;
   return $m;
}

sub readOFF {
   my($fn) = @_;
   my $p = { };
   
   my $err;

   my(@min,@max);
   $min[0] = $min[1] = $min[2] = 1e6;
   $max[0] = $max[1] = $max[2] = -1e6;
   
   if(open(my $fh,"<",$fn)) {
      my($nv,$nf); 
      my $t = 0;
      while(<$fh>) {
         s/\n$//;
         next if(/^\s*#/||/^\s*$/);
         $t += 2, $nv = $1, $nf = $2, next if($t==0 && /^OFF\s+(\d+)\s+(\d+)/);     # -- single line (OpenSCAD export like this)
         $t++, next if($t==0 && /^OFF$/);                                           # -- 1st line
         $t++, $nv = $1, $nf = $2, next if($t==1 && /^(\d+)\s+(\d+)/);              # -- 2nd line
         if($t==2 && $nv > 0) {
            s/^\s+//;
            my(@c) = split(/\s+/);
            @c = map { $_ * 1 } @c;
            push(@{$p->{vertices}},{c=>[$c[0],$c[1],$c[2]]});
            for(my $i=0; $i<3; $i++) {
               $min[$i] = $c[$i] if($min[$i]>$c[$i]);
               $max[$i] = $c[$i] if($max[$i]<$c[$i]);
            }
            $nv--;
         } elsif($t==2 && $nv == 0 && $nf > 0) {
            s/^\s+//;
            my(@v) = split(/\s+/);
            @v = map { $_ * 1 } @v;
            shift(@v);     # -- first count
            foreach my $i (0..$#v-2) {
               push(@{$p->{volumes}->[0]->{triangles}},{v=>[$v[0],$v[$i+1],$v[$i+2]]});
            }
            $nf--;
         } elsif($t==2 && $nv == 0 && $nf == 0) {
            last;
         }
      }
      close $fh;
      # -- keep min/max/size up-to-date
      $p->{min} = \@min;
      $p->{max} = \@max;
      $p->{size} = [$max[0]-$min[0],$max[1]-$min[1],$max[2]-$min[2]];
      return $p;
   }
}

sub writeOFF {
   my($fn,$m) = @_;
   my $fc = 0;

   open(my $fh,">",$fn);
   foreach my $v (@{$m->{volumes}}) {
      $fc += scalar @{$v->{triangles}};
   }
   print $fh "OFF\n\n".sprintf("%d %d %d\n",scalar @{$m->{vertices}},$fc,0);
   foreach my $v (@{$m->{vertices}}) {
      print $fh sprintf("%.5f %.5f %.5f\n",$v->{c}->[0],$v->{c}->[1],$v->{c}->[2]);
   }
   foreach my $v (@{$m->{volumes}}) {
      foreach my $f (@{$v->{triangles}}) {
         print $fh sprintf("3 %d %d %d\n",$f->{v}->[0],$f->{v}->[1],$f->{v}->[2]);
      }
   }
   close($fh);
}

sub readOBJ {
   my($fn) = @_;
   my $p = { };
   
   my $err;

   my(@min,@max);
   $min[0] = $min[1] = $min[2] = 1e6;
   $max[0] = $max[1] = $max[2] = -1e6;
   
   if(open(my $fh,"<",$fn)) {
      my(@vs);
      while(<$fh>) {
         chop;
         next if(/^\s*#/);
         if(/^v /) {
            my(@c) = split(/ +/);
            shift(@c);
            push(@{$p->{vertices}},{c=>[$c[0]*1,$c[1]*1,$c[2]*1]});
            for(my $i=0; $i<3; $i++) {
               $min[$i] = $c[$i] if($min[$i]>$c[$i]);
               $max[$i] = $c[$i] if($max[$i]<$c[$i]);
            }
         } elsif(/^f /) {
            my(@c) = split(/ +/);
            shift(@c);
            @c = map { $_-1 } @c;
            foreach my $i (0..$#c-2) {
               push(@{$p->{volumes}->[0]->{triangles}},{v=>[$c[0],$c[$i+1],$c[$i+2]]});
            }
         } elsif(/^\s*$/) {
            ;
         } elsif(/^(\S+)/) {
            print "WARN: '$1' in <$fn> not supported yet, ignored\n";
         }
      }
      close $fh;
      
      # -- keep min/max/size up-to-date
      $p->{min} = \@min;
      $p->{max} = \@max;
      $p->{size} = [$max[0]-$min[0],$max[1]-$min[1],$max[2]-$min[2]];
      #print toJSON($p);
      return $p;
   }
}

sub writeOBJ {
   my($fn,$p) = @_;
   if(open(my $fh,">",$fn)) {
      foreach my $v (@{$p->{vertices}}) {
         print $fh sprintf("v %.5f %.5f %.5f\n",$v->{c}->[0],$v->{c}->[1],$v->{c}->[2]);
      }
      foreach my $v (@{$p->{volumes}}) {
         foreach my $f (@{$v->{triangles}}) {
            print $fh sprintf("f %d %d %d\n",$f->{v}->[0]+1,$f->{v}->[1]+1,$f->{v}->[2]+1);
         }
      }
      close $fh;
   }
}


